package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/go-redis/redis"
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	log "github.com/sirupsen/logrus"
	"github.com/spf13/pflag"
)

type ContextConfig struct {
	MaxRows        uint `json:"maxRows,omitempty"`
	NanhuAccountID int  `json:"nanhuAccountId,omitempty"`
}

type Context struct {
	echo.Context
	db     *sqlx.DB
	cache  *redis.Client
	config ContextConfig
}

type ShipsInfoQuery struct {
	Where  map[string]interface{} `json:"where"`
	Offset uint                   `json:"offset"`
	Limit  uint                   `json:"limit"`
}

type ShipsInfoResult struct {
	Data  []Ship         `json:"data"`
	Query ShipsInfoQuery `json:"query"`
}

type ShipsStatusQuery struct {
	Where  map[string]interface{}
	Offset uint
	Limit  uint
}

type ShipsStatusResult struct {
	Data  []ShipStatus
	Query ShipsStatusQuery
}

func getShipsInfo(c echo.Context) error {
	cc := c.(*Context)
	qstr := cc.QueryParam("query")
	if len(qstr) == 0 {
		qstr = "{}"
	}
	var query ShipsInfoQuery
	err := json.Unmarshal([]byte(qstr), &query)
	if err != nil {
		return err
	}
	if query.Limit > cc.config.MaxRows {
		query.Limit = cc.config.MaxRows
	} else if query.Limit == 0 {
		query.Limit = 10
	}
	ships := []Ship{}
	err = cc.db.Select(
		&ships,
		"SELECT id, name, mac, captain_id, m_m_s_i FROM ship  WHERE account_id = ? LIMIT ? OFFSET ?",
		cc.config.NanhuAccountID,
		query.Limit,
		query.Offset)
	if err != nil {
		return err
	}
	res := ShipsInfoResult{
		Data:  ships,
		Query: query,
	}
	return c.JSON(http.StatusOK, res)
}

func encodeMac(mac string) string {
	mac = strings.Replace(mac, ":", "", -1)
	mac = strings.ToUpper(mac)
	return mac
}

func parseFloat(str string) float64 {
	v, err := strconv.ParseFloat(str, 64)
	if err != nil {
		return 0
	}
	return v
}

func getShipsStatus(c echo.Context) error {
	cc := c.(*Context)
	qstr := cc.QueryParam("query")
	if len(qstr) == 0 {
		qstr = "{}"
	}
	var query ShipsStatusQuery
	err := json.Unmarshal([]byte(qstr), &query)
	if err != nil {
		return err
	}
	if query.Limit > cc.config.MaxRows {
		query.Limit = cc.config.MaxRows
	} else if query.Limit == 0 {
		query.Limit = 10
	}
	ships := []struct {
		ID  int
		Mac string
	}{}
	err = cc.db.Select(&ships,
		"SELECT id, mac FROM ship WHERE account_id = ? LIMIT ? OFFSET ?",
		cc.config.NanhuAccountID,
		query.Limit,
		query.Offset)
	if err != nil {
		return err
	}
	statusList := []ShipStatus{}
	for _, ship := range ships {
		mac := encodeMac(ship.Mac)
		key := fmt.Sprintf("nanhu:shipStatus:%s", mac)
		// sstr, err := cc.cache.Get(key).Result()
		values, err := cc.cache.HGetAll(key).Result()
		if err != nil {
			log.WithFields(log.Fields{"values": values, "err": err}).Error("failed get values")
			values = map[string]string{}
		} else {
			log.WithFields(log.Fields{"values": values}).Info("get cached values")
		}
		onlineKey := fmt.Sprintf("nanhu:shipOnline:%s", mac)
		var isOnline bool
		str, err := cc.cache.Get(onlineKey).Result()
		if err != nil {
			log.WithFields(log.Fields{"value": str, "err": err}).Error("error get online cache")
			str = ""
			isOnline = false
		} else {
			log.WithFields(log.Fields{"value": str, "err": err}).Info("get online cache")
			isOnline = true
		}
		onlineTimestamp, err := time.Parse(time.RFC3339, str)
		if err != nil {
			onlineTimestamp = time.Unix(0, 0)
		}
		longitudeStr := values["longitude"]
		longitude := parseFloat(longitudeStr)
		latitudeStr := values["latitude"]
		latitude := parseFloat(latitudeStr)
		speedStr := values["speed_kmh"]
		speed := parseFloat(speedStr)
		trueTrack := parseFloat(values["true_track"])
		timestampStr := values["time"]
		timestamp, err := time.Parse("2006-01-02 15:04:05", timestampStr)
		if err != nil {
			timestamp = time.Unix(0, 0)
		}
		updatedAtStr := values["updatedAt"]
		updatedAt, err := time.Parse(time.RFC3339, updatedAtStr)
		if err != nil {
			updatedAt = time.Unix(0, 0)
		}
		offlineTimestamp := time.Unix(0, 0)
		if !isOnline {
			offlineTimestamp = updatedAt
		}
		s := ShipStatus{
			ID:               ship.ID,
			IsOnline:         isOnline,
			OnlineTimestamp:  onlineTimestamp,
			OfflineTimestamp: offlineTimestamp,
			Longitude:        longitude,
			Latitude:         latitude,
			SpeedKmph:        speed,
			TrueTrack:        trueTrack,
			Timestamp:        timestamp,
			UpdatedAt:        updatedAt,
		}
		statusList = append(statusList, s)
	}
	res := ShipsStatusResult{
		Data:  statusList,
		Query: query,
	}
	return c.JSON(http.StatusOK, res)
}

func getShipRoute(c echo.Context) error {
	// cc := c.(*Context)
	return c.JSON(http.StatusNotFound, map[string]string{"err": "not supported"})
}

type Status struct {
	ID        int       `json:"id,omitempty"`
	Longitude float64   `json:"longitude,omitempty"`
	Latitude  float64   `json:"latitude,omitempty"`
	Time      time.Time `json:"timestamp,omitempty"`
}

type ShipTrackQuery struct {
	From time.Time `json:"from"`
	To   time.Time `json:"to"`
}

type ShipTrackResult struct {
	Data  []Status       `json:"data"`
	Query ShipTrackQuery `json:"query"`
}

func getShipTrack(c echo.Context) error {
	cc := c.(*Context)
	shipIDStr := cc.Param("id")
	shipID, err := strconv.ParseUint(shipIDStr, 0, 32)
	if err != nil {
		errs := map[string]error{
			"err": err,
		}
		return c.JSON(http.StatusBadRequest, errs)
	}

	qstr := cc.QueryParam("query")
	if len(qstr) == 0 {
		qstr = "{}"
	}
	var query ShipTrackQuery
	err = json.Unmarshal([]byte(qstr), &query)
	if err != nil {
		return c.JSON(http.StatusBadRequest, map[string]error{"err": err})
	}

	withFrom := !query.From.IsZero()
	withTo := !query.To.IsZero()
	log.WithFields(log.Fields{"withFrom": withFrom, "withTo": withTo, "query": query}).Info("query")

	if withFrom && withTo {
		d := query.To.Sub(query.From)
		if d.Seconds() > 86400 {
			query.From = query.To.Add(-86400 * time.Second)
		}
	} else if withFrom {
		query.To = time.Now()
		d := query.To.Sub(query.From)
		if d.Seconds() > 86400 {
			query.To = query.From.Add(86400 * time.Second)
			withTo = true
		}
	} else if withTo {
		query.From = query.To.Add(-86400 * time.Second)
		withFrom = true
	} else {
		query.From = time.Now().Add(-86400 * time.Second)
		withFrom = true
	}

	var serr error
	var rows *sqlx.Rows
	var countRow *sqlx.Row
	table := fmt.Sprintf("shipgeo_%d", shipID)
	countSQL := fmt.Sprintf("SELECT COUNT(*) count FROM %s ", table)
	sql := fmt.Sprintf("SELECT id, longitude, latitude, time FROM %s ", table)
	if withFrom && withTo {
		countSQL = fmt.Sprintf(`%s WHERE time > ? AND time < ?`, countSQL)
		sql = fmt.Sprintf(`%s WHERE time > ? AND time < ?`, sql)
		countRow = cc.db.QueryRowx(countSQL, query.From, query.To)
		rows, serr = cc.db.Queryx(sql, query.From, query.To)
	} else if withFrom {
		countSQL = fmt.Sprintf(`%s WHERE time > ?`, countSQL)
		sql = fmt.Sprintf(`%s WHERE time > ?`, sql)
		countRow = cc.db.QueryRowx(countSQL, query.From)
		rows, serr = cc.db.Queryx(sql, query.From)
	} else if withTo {
		countSQL = fmt.Sprintf(`%s WHERE time < ?`, countSQL)
		sql = fmt.Sprintf(`%s WHERE time < ?`, sql)
		countRow = cc.db.QueryRowx(countSQL, query.To)
		rows, serr = cc.db.Queryx(sql, query.To)
	} else {
		countSQL = fmt.Sprintf(`%s`, countSQL) // nolint
		sql = fmt.Sprintf(`%s`, sql)           // nolint
		countRow = cc.db.QueryRowx(countSQL)
		rows, serr = cc.db.Queryx(sql)
	}
	var count int64
	err = countRow.Scan(&count)
	if err != nil {
		log.WithFields(log.Fields{"err": err, "countRow": countRow, "sql": countSQL}).Error("failed get count of rows")
		return c.JSON(http.StatusBadGateway, map[string]error{"err": err})
	}
	if serr != nil {
		log.WithFields(log.Fields{"err": serr, "sql": sql}).Error("failed get rows")
		return c.JSON(http.StatusBadGateway, map[string]error{"err": serr})
	}
	limit := int64(1000)
	skip := count / limit
	if skip == 0 {
		skip = 1
	}
	log.WithFields(log.Fields{"count": count, "skip": skip, "limit": limit}).Info("number of matching rows")
	trace := []Status{}
	var index int64
	for rows.Next() {
		if index%skip != 0 {
			index = index + 1
			continue
		}
		index = index + 1
		var s Status
		err := rows.StructScan(&s)
		if err != nil {
			log.WithFields(log.Fields{"err": err, "index": index, "status": s}).Error("error scan rows")
			return c.JSON(http.StatusBadGateway, map[string]error{"err": err})
		}
		trace = append(trace, s)
	}
	res := ShipTrackResult{
		Data:  trace,
		Query: query,
	}
	return c.JSON(http.StatusOK, res)
}

func initCache(config CacheConfig) *redis.Client {
	opts := &redis.Options{
		Addr: config.URI,
		DB:   config.DB,
	}
	client := redis.NewClient(opts)
	return client
}

func main() {
	cfgFile := pflag.StringP("config", "c", "", "")
	pflag.Parse()
	if *cfgFile == "" {
		fmt.Printf("must provide config file")
		os.Exit(-1)
	}
	config, err := loadConfig(*cfgFile)
	if err != nil {
		panic(err)
	}

	db := initDB(config.DBConfig)
	cache := initCache(config.CacheConfig)

	e := echo.New()
	e.Debug = true

	e.Use(func(h echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			cc := &Context{
				Context: c,
				db:      db,
				cache:   cache,
				config:  config.ContextConfig,
			}
			return h(cc)
		}
	})

	logger := middleware.LoggerWithConfig(middleware.LoggerConfig{})
	e.Use(logger)
	e.Use(middleware.Recover())

	e.Use(middleware.JWT([]byte("starnet2010")))

	prefix := config.APIPrefix
	e.GET(fmt.Sprintf("%s/ships/info", prefix), getShipsInfo)
	e.GET(fmt.Sprintf("%s/ships/status", prefix), getShipsStatus)
	e.GET(fmt.Sprintf("%s/ship/:id/route", prefix), getShipRoute)
	e.GET(fmt.Sprintf("%s/ship/:id/track", prefix), getShipTrack)
	routes := e.Routes()
	for _, r := range routes {
		log.WithFields(log.Fields{
			"name":   r.Name,
			"method": r.Method,
			"path":   r.Path,
		}).Info("registered route")
	}
	e.Logger.Fatal(e.Start(config.Listen))
}
